//
//  InAppReceipt.swift
//  SwiftyStoreKit
//
//  Created by phimage on 22/12/15.
// Copyright (c) 2015 Andrea Bizzotto (bizz84@gmail.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import Foundation

extension Date {

    init?(millisecondsSince1970: String) {
        guard let millisecondsNumber = Double(millisecondsSince1970) else {
            return nil
        }
        self = Date(timeIntervalSince1970: millisecondsNumber / 1000)
    }
}

extension ReceiptItem {

    public init?(receiptInfo: ReceiptInfo) {
        guard
            let productId = receiptInfo["product_id"] as? String,
            let quantityString = receiptInfo["quantity"] as? String,
            let quantity = Int(quantityString),
            let transactionId = receiptInfo["transaction_id"] as? String,
            let originalTransactionId = receiptInfo["original_transaction_id"] as? String,
            let purchaseDate = ReceiptItem.parseDate(from: receiptInfo, key: "purchase_date_ms"),
            let originalPurchaseDate = ReceiptItem.parseDate(from: receiptInfo, key: "original_purchase_date_ms")
            else {
                print("could not parse receipt item: \(receiptInfo). Skipping...")
                return nil
        }
        self.productId = productId
        self.quantity = quantity
        self.transactionId = transactionId
        self.originalTransactionId = originalTransactionId
        self.purchaseDate = purchaseDate
        self.originalPurchaseDate = originalPurchaseDate
        self.webOrderLineItemId = receiptInfo["web_order_line_item_id"] as? String
        self.subscriptionExpirationDate = ReceiptItem.parseDate(from: receiptInfo, key: "expires_date_ms")
        self.cancellationDate = ReceiptItem.parseDate(from: receiptInfo, key: "cancellation_date_ms")
        if let isTrialPeriod = receiptInfo["is_trial_period"] as? String {
            self.isTrialPeriod = Bool(isTrialPeriod) ?? false
        } else {
            self.isTrialPeriod = false
        }
    }

    private static func parseDate(from receiptInfo: ReceiptInfo, key: String) -> Date? {

        guard
            let requestDateString = receiptInfo[key] as? String,
            let requestDateMs = Double(requestDateString) else {
                return nil
        }
        return Date(timeIntervalSince1970: requestDateMs / 1000)
    }
}

// MARK: - receipt mangement
internal class InAppReceipt {

    /**
     *  Verify the purchase of a Consumable or NonConsumable product in a receipt
     *  - Parameter productId: the product id of the purchase to verify
     *  - Parameter inReceipt: the receipt to use for looking up the purchase
     *  - return: either notPurchased or purchased
     */
    class func verifyPurchase(
        productId: String,
        inReceipt receipt: ReceiptInfo
    ) -> VerifyPurchaseResult {

        // Get receipts info for the product
        let receipts = receipt["receipt"]?["in_app"] as? [ReceiptInfo]
        let receiptsInfo = filterReceiptsInfo(receipts: receipts, withProductId: productId)
        let nonCancelledReceiptsInfo = receiptsInfo.filter { receipt in receipt["cancellation_date"] == nil }

        let receiptItems = nonCancelledReceiptsInfo.flatMap { ReceiptItem(receiptInfo: $0) }
        // Verify that at least one receipt has the right product id
        if let firstItem = receiptItems.first {
            return .purchased(item: firstItem)
        }
        return .notPurchased
    }

    /**
     *  Verify the purchase of a subscription (auto-renewable, free or non-renewing) in a receipt. This method extracts all transactions mathing the given productId and sorts them by date in descending order, then compares the first transaction expiry date against the validUntil value.
     *  - parameter type: .autoRenewable or .nonRenewing(duration)
     *  - Parameter productId: the product id of the purchase to verify
     *  - Parameter inReceipt: the receipt to use for looking up the subscription
     *  - Parameter validUntil: date to check against the expiry date of the subscription. If nil, no verification
     *  - Parameter validDuration: the duration of the subscription. Only required for non-renewable subscription.
     *  - return: either NotPurchased or Purchased / Expired with the expiry date found in the receipt
     */
    class func verifySubscription(
        type: SubscriptionType,
        productId: String,
        inReceipt receipt: ReceiptInfo,
        validUntil date: Date = Date()
    ) -> VerifySubscriptionResult {

        // The values of the latest_receipt and latest_receipt_info keys are useful when checking whether an auto-renewable subscription is currently active. By providing any transaction receipt for the subscription and checking these values, you can get information about the currently-active subscription period. If the receipt being validated is for the latest renewal, the value for latest_receipt is the same as receipt-data (in the request) and the value for latest_receipt_info is the same as receipt.
        let (receipts, duration) = getReceiptsAndDuration(for: type, inReceipt: receipt)
        let receiptsInfo = filterReceiptsInfo(receipts: receipts, withProductId: productId)
        let nonCancelledReceiptsInfo = receiptsInfo.filter { receipt in receipt["cancellation_date"] == nil }
        if nonCancelledReceiptsInfo.count == 0 {
            return .notPurchased
        }

        let receiptDate = getReceiptRequestDate(inReceipt: receipt) ?? date

        let receiptItems = nonCancelledReceiptsInfo.flatMap { ReceiptItem(receiptInfo: $0) }

        if nonCancelledReceiptsInfo.count > receiptItems.count {
            print("receipt has \(nonCancelledReceiptsInfo.count) items, but only \(receiptItems.count) were parsed")
        }

        let sortedExpiryDatesAndItems = expiryDatesAndItems(receiptItems: receiptItems, duration: duration).sorted { a, b in
            return a.0 > b.0
        }

        guard let firstExpiryDateItemPair = sortedExpiryDatesAndItems.first else {
            return .notPurchased
        }

        let sortedReceiptItems = sortedExpiryDatesAndItems.map { $0.1 }
        if firstExpiryDateItemPair.0 > receiptDate {
            return .purchased(expiryDate: firstExpiryDateItemPair.0, items: sortedReceiptItems)
        } else {
            return .expired(expiryDate: firstExpiryDateItemPair.0, items: sortedReceiptItems)
        }
    }

    private class func expiryDatesAndItems(receiptItems: [ReceiptItem], duration: TimeInterval?) -> [(Date, ReceiptItem)] {

        if let duration = duration {
            return receiptItems.map {
                let expirationDate = Date(timeIntervalSince1970: $0.originalPurchaseDate.timeIntervalSince1970 + duration)
                return (expirationDate, $0)
            }
        } else {
            return receiptItems.flatMap {
                if let expirationDate = $0.subscriptionExpirationDate {
                    return (expirationDate, $0)
                }
                return nil
            }
        }
    }

    private class func getReceiptsAndDuration(for subscriptionType: SubscriptionType, inReceipt receipt: ReceiptInfo) -> ([ReceiptInfo]?, TimeInterval?) {
        switch subscriptionType {
        case .autoRenewable:
            return (receipt["latest_receipt_info"] as? [ReceiptInfo], nil)
        case .nonRenewing(let duration):
            return (receipt["receipt"]?["in_app"] as? [ReceiptInfo], duration)
        }
    }

    private class func getReceiptRequestDate(inReceipt receipt: ReceiptInfo) -> Date? {

        guard let receiptInfo = receipt["receipt"] as? ReceiptInfo,
            let requestDateString = receiptInfo["request_date_ms"] as? String else {
            return nil
        }
        return Date(millisecondsSince1970: requestDateString)
    }

    /**
     *  Get all the receipts info for a specific product
     *  - Parameter receipts: the receipts array to grab info from
     *  - Parameter productId: the product id
     */
    private class func filterReceiptsInfo(receipts: [ReceiptInfo]?, withProductId productId: String) -> [ReceiptInfo] {

        guard let receipts = receipts else {
            return []
        }

        // Filter receipts with matching product id
        let receiptsMatchingProductId = receipts
            .filter { (receipt) -> Bool in
                let product_id = receipt["product_id"] as? String
                return product_id == productId
            }

        return receiptsMatchingProductId
    }
}
